#! /usr/bin/python3
# coding=utf-8
# @Time：2025/8/4 0:13
# @Author: Shawn
# @introduction: 通过图形揭界面化，模拟cni设备连接阿里云iot
import datetime
import json
import os
import sys
import time

from PyQt5.QtCore import Qt, QObject, pyqtSignal
from PyQt5.QtGui import QPalette, QColor, QFont, QTextCursor, QTextCharFormat
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget, QSplitter, QMainWindow, \
    QTextEdit, QComboBox, QLabel, QLineEdit, QGroupBox, QMessageBox

from aliyun_mqtt_client import AliYunMQTTClient
from get_device_triples_info import get_device_triples_info


# 创建线程安全的日志转发器
class LogEmitter(QObject):
    log_signal = pyqtSignal(str)


class CniIotDevice(QMainWindow):

    def __init__(self):
        super().__init__()
        # 三元组信息
        self.host = None
        self.scada_sn = None
        self.product_key = None
        self.device_secret = None

        self.ems_text_input = None
        self.mqtt_client = None
        self.env_combo = None
        self.get_btn_connect = None
        self.data_display = QTextEdit()
        # 版本：V1.01：更新点：收发区时间格式新增毫秒级
        self.setWindowTitle("模拟工商业设备连接iot阿里云平台V1.01")
        self.resize(1200, 900)
        # self.setMinimumSize(400, 300)  # 设置最小尺寸但不限制最大尺寸
        # self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        # # 方法2：确保最大化按钮启用（可选）
        # self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint)

        # 初始化变量（没有用到）
        self.current_image = None
        self.original_image = None
        self.image_path = ""
        self.test_results = []

        # topic和topic中的msg加载数据文件路径
        self.data_file = "app_data.json"

        self.topic_combo = QComboBox()  # 打包变动

        # 状态栏
        self.statusBar().showMessage("就绪 | 欢迎使用测试工具")

        # 创建UI
        self.create_ui()

        # 确保UI组件已初始化后再加载数据
        self.data_dict = self.load_data()

        # 初始化topic_combo的数据源
        self.topic_combo.addItems(self.data_dict.keys())

        # 初始显示第一个选项的数据：(注意：必须要在create_ui()初始化以后才能初始化update_text(0))# topic和topic中的msg加载数据
        self.update_topic_msg(0)

        self.log_emitter = LogEmitter()
        self.log_emitter.log_signal.connect(self.display_data)  # 连接信号到显示方法

    def create_ui(self):
        # 主布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # 使用分割器布局
        splitter = QSplitter(Qt.Horizontal)
        main_layout.addWidget(splitter, 1)  # 添加分割器到主布局

        # 左侧控制面板
        control_panel = QWidget()
        control_layout = QVBoxLayout(control_panel)
        control_layout.setContentsMargins(1, 1, 1, 1)
        control_layout.setSpacing(200)

        # 右侧数据显示区域
        display_panel = QWidget()
        display_layout = QVBoxLayout(display_panel)
        display_layout.setContentsMargins(1, 1, 1, 1)

        # 添加组件到分割器
        splitter.addWidget(control_panel)
        splitter.addWidget(display_panel)
        splitter.setSizes([300, 900])

        # ===== 左侧控制面板内容 =====
        # 分组框：环境设置
        env_group = QGroupBox()
        env_layout = QVBoxLayout(env_group)
        env_layout.setContentsMargins(10, 15, 10, 15)
        env_layout.setSpacing(10)

        # 添加分组框到控制面板
        control_layout.addWidget(env_group)
        control_layout.addStretch(1)  # 添加弹性空间

        # 环境选择
        env_label = QLabel("选择环境:")
        env_label.setStyleSheet("font-weight: bold;")
        # 环境选择框
        self.env_combo = QComboBox()
        self.env_combo.addItems([
            "测试：http://cnicloud.alphaess.com:8886",
            "测试：https://cnicloud.alphaess.com:8877",
            "正式：http://cniiot.alphaess.com",
            "5.0灰度sg：https://huidu-sgems.alphaess.com",
            "5.0灰度eu：https://huidu-eurems.alphaess.com",
        ])
        self.env_combo.setMinimumHeight(30)

        # scada_sn输入框
        sn_label = QLabel("SCADA_SN：")
        sn_label.setStyleSheet("font-weight: bold;")
        self.scada_text_input = QLineEdit()
        self.scada_text_input.setPlaceholderText("请输入scada_sn")
        self.scada_text_input.setMinimumHeight(30)

        # 获取设备三元组连接iot信息按钮
        self.get_btn_connect = QPushButton("获取设备三元组信息")
        self.get_btn_connect.clicked.connect(self.click_get_device_triples_info)
        self.get_btn_connect.setMinimumHeight(20)

        # 连接IOT按钮
        self.connect_iot_button = QPushButton("连接IOT")
        self.connect_iot_button.clicked.connect(self.connect_iot)
        self.connect_iot_button.setMinimumHeight(20)

        # 断开连接IOT按钮
        self.stop_connect_iot_button = QPushButton("断开连接")
        self.stop_connect_iot_button.clicked.connect(self.stop_connect)
        self.stop_connect_iot_button.setMinimumHeight(20)

        # ems_sn输入框
        ems_label = QLabel("EMS_SN：")
        ems_label.setStyleSheet("font-weight: bold;")
        self.ems_text_input = QLineEdit()
        self.ems_text_input.setPlaceholderText("请输入EMS_SN")
        self.ems_text_input.setMinimumHeight(30)

        # topic选择框提示
        type_label = QLabel("选择topic：")
        type_label.setFont(QFont("Arial", 10))
        type_label.setStyleSheet("font-weight: bold;")
        # topic选择框
        self.topic_combo = QComboBox()
        # self.topic_combo.addItems(self.data_dict.keys()) # 初始化了，不需要了
        self.topic_combo.setFont(QFont("Arial", 10))
        self.topic_combo.setMinimumHeight(30)
        # 绑定型号槽
        self.topic_combo.currentIndexChanged.connect(self.update_topic_msg)

        # topic对应的msg文本框
        self.topic_msg_edit = QTextEdit()
        # 禁用富文本粘贴
        self.topic_msg_edit.setAcceptRichText(False)
        self.topic_msg_edit.setFont(QFont("Consolas", 10))
        # 创建默认格式
        topic_msg_edit = QTextCharFormat()
        topic_msg_edit.setForeground(Qt.white)
        # 应用默认格式
        self.topic_msg_edit.setCurrentCharFormat(topic_msg_edit)
        # topic_msg_edit文本框布局
        display_layout.addWidget(self.topic_msg_edit, 1)

        # 发送topic数据按钮
        self.send_topic_data = QPushButton("发送")
        # 点击【发送】按钮，发送topic和对应topic的msg
        self.send_topic_data.clicked.connect(self.send_publish)
        self.send_topic_data.setMinimumHeight(20)

        # 设备收发数据文本框
        self.data_display = QTextEdit()
        self.data_display.setFont(QFont("Consolas", 10))
        # 创建默认格式
        default_format = QTextCharFormat()
        default_format.setForeground(Qt.white)
        # 应用默认格式
        self.data_display.setCurrentCharFormat(default_format)
        # 禁用富文本粘贴
        self.data_display.setAcceptRichText(False)
        self.data_display.setPlaceholderText("获取的数据将显示在此处...")
        main_layout.addWidget(self.data_display, 1)  # 添加弹性因子使文本框填充空间

        # 添加连接组件
        env_layout.addWidget(env_label)  # 环境选择 提示语
        env_layout.addWidget(self.env_combo)  # 环境选择框
        env_layout.addWidget(sn_label)  # scada_sn输入框提示语
        env_layout.addWidget(self.scada_text_input)  # scada_sn输入框
        env_layout.addWidget(self.get_btn_connect)  # 获取设备三元组信息 按钮
        env_layout.addWidget(self.connect_iot_button)  # 【连接IOT】按钮
        env_layout.addWidget(ems_label)  # ems_sn输入框提示语
        env_layout.addWidget(self.ems_text_input)  # ems_sn输入框
        env_layout.addWidget(type_label)  # 【选择topic】提示语
        env_layout.addWidget(self.topic_combo)  # topic下拉选择框
        env_layout.addWidget(self.send_topic_data)  # 【发送】按钮
        env_layout.addWidget(self.stop_connect_iot_button)  # 【断开连接】按钮

    def click_get_device_triples_info(self):
        """
        获取设备三元组信息
        :return:
        """
        # 获取下拉框中的环境选择数据
        env_url = self.env_combo.currentText()[self.env_combo.currentText().find("：") + 1:]

        # 获取scada_sn数据框输入的数据
        scada_sn = self.scada_text_input.text().strip()
        # 获取设备三元组信息
        data = get_device_triples_info(scada_sn, env_url)

        # 三元组信息返回格式不对时(返回的数据没有“;”符号)，return
        if ";" not in data:
            self.display_data(data)
            return
        parts = data.split(';')
        # 提取分割后的三部分数据+scada_sn
        self.host = parts[0]  # 第一部分：主机地址
        self.product_key = parts[1]  # 第二部分：客户端ID
        self.device_secret = parts[2]  # 第三部分：密码/密钥
        self.scada_sn = scada_sn

        # 企业版上海测试的scada_sn设备
        if self.product_key == "k0nd9zH6Bt4":
            device_location_iot = "设备为SCADA_CI，在企业版上海"
        # 企业版上海正式的EMS-CI设备
        elif self.product_key == "k0nd9fACqWL":
            device_location_iot = "设备为EMS-CI，在企业版上海"
        # 企业版上海测试的BMS设备
        elif self.product_key == "k0nd9jWSnDV":
            device_location_iot = "设备为BMS_TEST，在企业版上海"
        # 企业版上海正式的BMS设备
        elif self.product_key == "k0nd9a3F3ST":
            device_location_iot = "设备为BMS-CI，在企业版上海"
        # 企业版新加坡正式的EMS_CI设备
        elif self.product_key == "k0ndaeKJU6G":
            device_location_iot = "设备为EMS_CI，在企业版新加坡"
        # 企业版新加坡正式的BMS_CI设备
        elif self.product_key == "k0ndaVN2NO8":
            device_location_iot = "设备为BMS_CI，在企业版新加坡"
        # 未找到设备在iot中的位置
        else:
            device_location_iot = "未找到设备在iot中的位置"
        # 把获取到的设备的三元组信息显示在文本框中
        self.display_data("成功获取" + self.scada_sn + "设备的三元组信息为：" + str(data) + "\t" + device_location_iot)

    def connect_iot(self):
        """连接阿里云iot"""
        try:
            # 必须要获取host的设备的三元组信息，才能连接IOT
            if self.host and self.product_key and self.scada_sn and self.device_secret:
                if not self.mqtt_client:
                    # 传递日志回调函数（通过信号确保线程安全）
                    def safe_log(message):
                        self.log_emitter.log_signal.emit(message)

                    self.mqtt_client = AliYunMQTTClient(host=self.host, product_key=self.product_key,
                                                        device_name=self.scada_sn,
                                                        device_secret=self.device_secret,
                                                        log_callback=safe_log  # 传入安全的日志回调
                                                        )
                    self.mqtt_client.start()
                    print("连接iot走完")
                    # 连接IOT成功以后，需要在文本框中打印连接成功状态
                    while True:
                        time.sleep(0.5)
                        if self.mqtt_client.is_connected:
                            break
                    print("连接IOT成功")
                elif not self.mqtt_client.is_connected:
                    self.display_data("请点击【断开连接】按钮以后，点击【连接IOT】重新连接")

                else:
                    self.display_data("设备" + self.scada_sn + "已经成功连接阿里云IOT，请勿重复操作【连接IOT】")
            else:
                self.display_data("请先操作【获取设备的三元组信息】，再操作【连接IOT】")
        except Exception as e:
            self.display_data(str(e))

    def stop_connect(self):
        """断开连接"""
        try:
            if self.mqtt_client:
                self.mqtt_client.stop()
                while True:
                    if not self.mqtt_client.is_connected:
                        self.mqtt_client = None
                        # self.display_data("断开连接成功")
                        break
            # self.mqtt_client为None时，说明没有实例化mqtt_client，没有操作连接IOT功能
            else:
                self.display_data("请先操作【连接IOT】")
        except Exception as e:
            self.display_data(f"断开连接错误，错误原因：{e}")

    def send_publish(self):
        time_now = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
        try:
            if self.mqtt_client:
                if self.mqtt_client.is_connected:
                    """发送topic对应的msg"""
                    topic_info = self.topic_combo.currentText()  # 获取下拉选择框的topic_combo选项中的topic
                    # 获取topic中的msg
                    topic_msg = self.topic_msg_edit.toPlainText().strip()
                    topic_msg = json.loads(topic_msg)
                    if isinstance(topic_msg, dict):
                        # 如果值匹配目标值，直接替换
                        def replace_value(obj):
                            if isinstance(obj, dict):
                                for key in obj:
                                    # 如果值匹配目标值，直接替换
                                    if obj[key] == "$Time":
                                        obj[key] = time_now
                                    # 如果值匹配目标值"$EmsSn"，直接替换输入框中的ems_sn
                                    if obj[key] == "$EmsSn":
                                        if self.ems_text_input.text().strip():
                                            # 获取ems_sn数据框输入的数据
                                            obj[key] = self.ems_text_input.text().strip()
                                        else:
                                            self.display_data("使用了“$EmsSn”变量写法，请在【ems_sn】输入框中填写正确数据")
                                            return False
                                    else:
                                        replace_value(obj[key])  # 递归处理子字典
                            elif isinstance(obj, list):
                                for item in obj:
                                    replace_value(item)  # 递归处理列表项
                            return True
                    else:
                        return self.display_data("传入的数据不是json格式")

                    if not replace_value(topic_msg):  # 执行替换
                        return

                    # 只需要topic中的数据，不需要-后面的解释语
                    topic = topic_info[0:topic_info.find("-")]
                    print(topic)
                    if topic == "inform" or topic == "progress":
                        complete_topic = f"/ota/device/{topic}/{self.product_key}/{self.scada_sn}"
                    else:
                        complete_topic = f"/{self.product_key}/{self.scada_sn}/user/" + topic
                    self.mqtt_client.publish(complete_topic, topic_msg)
                    # 发送成功以后，保存topic下的msg数据 todo：需要判断发送成功的逻辑
                    self.save_current_data()
                else:
                    self.display_data("请点击【断开连接】按钮以后，点击【连接IOT】重新连接")
            else:
                self.display_data("请先操作【连接IOT】")
        except Exception as e:
            # 弹窗提示，这样程序不会崩
            QMessageBox.critical(self, "保存失败", f"传入的数据格式不是json格式: {str(e)}")
            return False

    def load_data(self):
        """加载数据，如果文件不存在则返回：数据格式错误"""
        try:
            if getattr(sys, 'frozen', False):
                base_path = sys._MEIPASS
            else:
                base_path = os.path.dirname(os.path.abspath(__file__))

            data_file = os.path.join(base_path, "app_data.json")

            if os.path.exists(data_file):
                try:
                    with open(data_file, 'r', encoding='utf-8') as f:
                        return json.load(f)
                except Exception as e:
                    # 只在UI初始化后显示错误
                    if hasattr(self, 'data_display'):
                        self.display_data(f"app_data文件读取失败: {str(e)}")
                    return {}
            else:
                # 只在UI初始化后显示错误
                if hasattr(self, 'data_display'):
                    self.display_data(f"app_data文件不存在: {data_file}")
                return {}
        except Exception as e:
            # 只在UI初始化后显示错误
            if hasattr(self, 'data_display'):
                self.display_data(f"加载数据时发生错误: {str(e)}")
            return {}
        #
        # """加载数据，如果文件不存在则返回：数据格式错误"""
        # if os.path.exists(self.data_file):
        #     try:
        #         with open(self.data_file, 'r', encoding='utf-8') as f:
        #             return json.load(f)
        #     except:
        #         # 文件读取失败，使用默认数据
        #         self.display_data("app_data文件读取失败")
        # else:
        #     self.display_data("app_data文件打开失败")

    def update_topic_msg(self, index):
        """topic选择栏位切换时，更新topic_msg文本框显示的内容"""
        # currentText()获取下拉框中的数据
        selected_key = self.topic_combo.currentText()
        content = self.data_dict.get(selected_key, "")
        self.topic_msg_edit.setText(str(content))

    def save_current_data(self):
        """保存当前编辑的数据"""
        selected_key = self.topic_combo.currentText()
        # toPlainText()设置控件的纯文本内容
        new_content = self.topic_msg_edit.toPlainText().strip()

        # 更新数据字典
        self.data_dict[selected_key] = new_content

        # 保存到文件
        self.save_data()

    def save_data(self):
        """保存数据到文件"""
        try:
            with open(self.data_file, 'w', encoding='utf-8') as f:
                json.dump(self.data_dict, f, ensure_ascii=False, indent=4)
            return True
        except Exception as e:
            QMessageBox.critical(self, "保存失败", f"无法保存数据: {str(e)}")
            return False

    def closeEvent(self, event):
        """处理窗口关闭事件"""
        # 在关闭前保存当前编辑的数据
        self.save_current_data()

        # 显示关闭提示
        QMessageBox.information(self, "感谢使用", "您的topic_msg已自动保存，下次打开将恢复。")
        event.accept()

    def display_data(self, data):
        """在文本框中显示数据"""
        # self.data_display.clear() # 清除文本输入框数据
        self.data_display.append(self.get_now_date_time_str(time_accuracy=True) + "：" + data)
        self.data_display.append("")

        # 滚动到顶部:QTextCursor.Start，结尾：QTextCursor.End
        cursor = self.data_display.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.data_display.setTextCursor(cursor)  # 修改后的光标对象应用到文本编辑框中

    def clear_display(self):
        """清空数据展示框"""
        self.data_display.clear()
        # self.status_label.setText("已清空数据")

    def get_now_date_time_str(self, time_accuracy: bool = False):
        """
        :param time_accuracy: False：时间精度不保留三位毫秒级，True：时间精度保留三位毫秒级
        :return:
        """
        if time_accuracy:
            get_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
        else:
            get_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        return get_time


if __name__ == '__main__':
    app = QApplication(sys.argv)

    # 设置应用样式
    app.setStyle("Fusion")

    # 深色主题
    dark_palette = QPalette()
    dark_palette.setColor(QPalette.Window, QColor(53, 53, 53))
    dark_palette.setColor(QPalette.WindowText, Qt.white)
    dark_palette.setColor(QPalette.Base, QColor(35, 35, 35))
    dark_palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
    dark_palette.setColor(QPalette.ToolTipBase, Qt.white)
    dark_palette.setColor(QPalette.ToolTipText, Qt.white)
    dark_palette.setColor(QPalette.Text, Qt.white)
    dark_palette.setColor(QPalette.Button, QColor(53, 53, 53))
    dark_palette.setColor(QPalette.ButtonText, Qt.white)
    dark_palette.setColor(QPalette.BrightText, Qt.red)
    dark_palette.setColor(QPalette.Highlight, QColor(142, 45, 197).lighter())
    dark_palette.setColor(QPalette.HighlightedText, Qt.black)
    app.setPalette(dark_palette)

    # 设置字体
    font = QFont("Segoe UI", 10)
    app.setFont(font)

    window = CniIotDevice()
    window.show()

    sys.exit(app.exec_())
